查看原文
其他

CSS MASK 实现鼠标跟随镂空效果

XboxYan 前端侦探 2023-07-16

之前在某社区看到这样过一个问题,如何使一个div的部分区域变透明而其他部分模糊掉?[1],最后的实现效果如下

进一步,还能实现任意形状的镂空效果(微信压缩的有点过分了,建议查看 demo)

特点是鼠标经过的地方清晰可见,其他地方则是模糊的

可能很多同学一开始无从下手,没有思路,没关系,不要急,可以先从简单的、类似的效果开始,一步一步尝试,一起看看吧。

一、普通半透明效果

平时开发中可能还会碰到一种半透明的效果,有点类似于探照灯(鼠标之外是半透明遮罩,看起来会暗一点),效果如下:

和文章开头有点类似吧?那先从这种效果开始,假设布局是这样的:

<div class="wrap">
    <img class="prew" src="https://tva1.sinaimg.cn/large/008i3skNgy1gubr2sbyqdj60xa0m6tey02.jpg">
</div>

那么如何绘制一个中间镂空的圆呢?下面介绍两种方法

1. 阴影扩展实现

其实很简单,需要一点点想象力,一个足够大的阴影就可以了,原理如下

为了结构更加精简,这里可以用伪元素::before来绘制。用代码实现就是

.wrap::before{
  content:'';
  position: absolute;
  width100px;
  height100px;
  border-radius50%;
  left50%;
  top50%;
  transformtranslate(-50%,-50%);
  box-shadow0 0 0 999vw rgba(000, .5); /*一个足够大的投影*/
}

这样就得到了以下效果


2. 径向渐变实现

除了上述阴影扩展的方式,利用 CSS 径向渐变 也能实现这样的效果

需要绘制一个从透明到半透明的渐变,关键代码如下

.wrap::before{
     /**/
    backgroundradial-gradient( circle at center, transparent 50px, rgba(0,0,0,.551px);
}

效果如下


二、借助 CSS 变量传递鼠标位置

按照之前的经验,可能会在 js 中直接修改目标元素的 style 属性,比如像这样的操作

img.addEventListener('mousemove', (ev) => {
   img.style.left = 'XXpx';
   img.style.top = 'XXpx';
})

虽然也能实现想要的效果,但是这样交互与业务逻辑混合在一起,不利于后面的维护。其实,我们只需要通过 js 拿到鼠标的坐标就行了(CSS没有这样的能力),然后剩下的在 CSS 中处理就行了。

那如何传递呢?

这里借助 CSS 变量,那一切就好办了!假设鼠标的坐标是 [--x,--y](范围是[0, 1]),那么遮罩的坐标就可以使用 calc计算了

.wrap::before{
  leftcalc( var(--x) * 100% );
  topcalc( var(--y) * 100% );
}

JS 获取比较容易,关键代码如下

img.addEventListener('mousemove', (ev) => {
    img.style.setProperty('--x', ev.offsetX / ev.target.offsetWidth);
    img.style.setProperty('--y', ev.offsetY / ev.target.offsetHeight);
})

这样一来,半透明效果的镂空效果就完成了,实时效果如下(注意观察--x 的变化)

完整代码可以访问:backdrop-shadow (codepen.io)[2]

再来说第二种径向渐变的方案。

从这里就可以看出 CSS 变量的好处了,无需修改 JS,只需要在CSS中修改渐变中心点的位置就可以实现了(at 关键词),关键代码如下

.wrap::before{
  backgroundradial-gradient( circle at calc(var(--x) * 100% )  calc(var(--y) * 100% ), transparent 50pxrgba(0,0,0,.551px);
}

三、背景模糊尝试

在 CSS 中,有一个专门针对背景(元素背后区域)的滤镜:backdrop-filter[3]

backdrop-filterblur(10px);

下面是 MDN 中的一个示意效果

backdrop-filter是让当前元素所在区域后面的内容模糊,要想看到效果,需要元素本身半透明或者完全透明;而filter是让当前元素自身模糊。有兴趣的可以查看这篇文章:CSS backdrop-filter简介与苹果iOS毛玻璃效果[4]

现在尝试一下

1. 阴影扩展实现

如果在上面第一个例子中添加 backdrop-filter

.wrap::before{
  /**/
  left50%;
  top50%;
  transformtranslate(-50%,-50%); /*默认居中*/
  box-shadow0 0 0 999vw rgba(000, .5); /*足够大的投影*/
  backdrop-filterblur(5px)
}

实时效果如下

可以看到鼠标区域是模糊的,外面是清晰的,正好和希望的效果相反。原因是,圆形区域才是真实的结构,外面都是阴影,是不存在的,所以最后滤镜的作用范围只有圆形部分

2. 径向渐变实现

如果在第二个例子中添加 backdrop-filter

.wrap::before{
     /**/    backgroundradial-gradient( circle at calc(var(--x) * 100% )  calc(var(--y) * 100% ), transparent 50pxrgba(0,0,0,.551px);
   backdrop-filterblur(5px)
}

实时效果如下(微信压缩有点厉害)

可惜的是,已经全部都模糊了,只是鼠标区域外面暗一些。这是由于::before的尺寸占据整个容器,所以整个背后都变模糊了,圆形外部比较暗是因为半透明渐变的影响。

四、CSS MASK 实现镂空效果

MASK 的原理和现实生活比较像,你可以想象成镂空,就好比一整块磨砂玻璃,然后通过 CSS MASK 打了一个圆孔,这样透过圆孔看到后面肯定是清晰的。

然后对第二个例子稍作修改,把这个径向渐变作为层,示意如下

用代码实现就是

.wrap::before{
   -webkit-maskradial-gradient( circle at calc(var(--x, .5) * 100% )  calc(var(--y, .5) * 100% ), transparent 50px#000 51px);
   backgroundrgba(0,0,0,.3);
   backdrop-filterblur(5px)
}

这样就实现了文章开头的效果

完整代码可以查看:backdrop-mask (codepen.io)[5]

五、借助 MASK COMPOSITE 实现更灵活的镂空效果

除了使用径向渐变绘制遮罩层以外,还可以通过 CSS MASK COMPOSITE(遮罩合成)[6]的方式来实现,这种方式更为灵活,可以扩展其他形状的效果。

标准关键值如下(可惜只有firefox支持):

mask-compositeadd/* 叠加(默认值) */
mask-compositesubtract/* 减去,排除掉上层的区域 */
mask-compositeintersect/* 相交,只显示重合的地方 */
mask-compositeexclude/* 排除,只显示不重合的地方 */

MASK COMPOSITE 是什么意思呢?可以类比设计软件中的形状合成,几乎是一一对应的,下面是 photoshop 的效果示意

-webkit-mask-composite[7] 与标准下的值有所不同,属性值非常多(chrome 和 safari 支持),如下

-webkit-mask-compositeclear/*清除,不显示任何遮罩*/
-webkit-mask-compositecopy/*只显示上方遮罩,不显示下方遮罩*/
-webkit-mask-compositesource-over
-webkit-mask-compositesource-in/*只显示重合的地方*/
-webkit-mask-compositesource-out/*只显示上方遮罩,重合的地方不显示*/
-webkit-mask-compositesource-atop;
-webkit-mask-compositedestination-over;
-webkit-mask-compositedestination-in/*只显示重合的地方*/
-webkit-mask-compositedestination-out;/*只显示下方遮罩,重合的地方不显示*/
-webkit-mask-compositedestination-atop;
-webkit-mask-compositexor/*只显示不重合的地方*/

是不是一脸懵了?其实功能是完全覆盖了标准属性,这里做了一个各个属性对应的效果图,如下

现在回到这里,可以绘制一整块背景(线性渐变实现)和一个圆形背景(径向渐变实现),然后通过遮罩合成排除(mask-composite: exclude)打一个孔就行了,实现如下

.wrap::before{
   -webkit-maskurl("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='25' cy='25' r='25' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
   -webkit-mask-size50px100%;
   -webkit-mask-repeat: no-repeat;
   -webkit-mask-positioncalc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px )  calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
   -webkit-mask-composite: xor; /*只显示不重合的地方, chorem 、safari 支持*/
   mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
   backgroundrgba(0,0,0,.3);
   backdrop-filterblur(5px)
}

需要注意-webkit-mask-position中的计算,可以充分利用 calc 的特性,实时效果如下

完整代码可以查看:backdrop-mask-composite (codepen.io)[8]

可能你已经发现,上述例子中的圆是通过 svg 绘制(已经进行 CSS 转义)的,还用到了遮罩合成,看着好像比前面的方式更加繁琐了。

为什么这么做呢?其实,这是一种更加万能的解决方式,可以带来无限的可能性。比如我现在需要一个星星的镂空效果,很简单,先通过一个绘制软件画一个,下面是 Figma

然后把这段 svg 代码转义一下,这里推荐使用张鑫旭老师的SVG在线压缩合并工具[9],然后把刚才圆形的 svg 替换成这个星形的svg 就可以了

.wrap::before{
     /**/   -webkit-maskurl("data:image/svg+xml,%3Csvg width='96' height='91' viewBox='0 0 96 91' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M48 0l11.226 34.55h36.327l-29.39 21.352L77.39 90.45 48 69.098 18.61 90.451 29.837 55.9.447 34.55h36.327L48 0z' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
   -webkit-mask-size50px100%;
   -webkit-mask-repeat: no-repeat;
   -webkit-mask-positioncalc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px )  calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
   -webkit-mask-composite: xor;   /*只显示不重合的地方, chorem 、safari 支持*/
   mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
   backgroundrgba(0,0,0,.3);
   backdrop-filterblur(5px)
}

下面是星星镂空效果

Kapture 2021-11-20 at 13.35.28

完整代码可以查看:backdrop-star (codepen.io)[10]

再比如想要一个心形❤,也采用同样的方式,实现效果如下

Kapture 2021-11-20 at 13.44.26

完整代码可以查看:backdrop-heart (codepen.io)[11]

只要你能想到的,都可以实现,是不是非常神奇?

六、简单总结一下

以上实现了一个鼠标跟随镂空的效果,从简单到复杂,从单一到通用,虽然借助了一点点 JS ,但是仅仅是“工具”的角色,交互逻辑全部都由 CSS 完成,而且根据灵活,适应性更强,下面总结一下要点:

  1. 足够大的阴影是一个实现圆形镂空效果的小技巧
  2. CSS 渐变也能轻易的绘制出圆形镂空背景
  3. 借助 CSS 变量可以很方便的利用鼠标位置实现想要的效果
  4. backdrop-filter 可以想象成磨砂玻璃的功能
  5. CSS Mask 可以给磨砂玻璃打孔,实现镂空的效果
  6. 借助遮罩合成特性和SVG,可以实现任意形状的镂空效果

CSS MASK 还是非常强大的,有必要还是多掌握一下。最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发❤❤❤

参考资料

[1]

如何使一个div的部分区域变透明而其他部分模糊掉?: https://segmentfault.com/q/1010000040902161

[2]

backdrop-shadow (codepen.io): https://codepen.io/xboxyan/pen/VwzRaNZ

[3]

backdrop-filter: https://developer.mozilla.org/zh-CN/docs/Web/CSS/backdrop-filter

[4]

CSS backdrop-filter简介: https://www.zhangxinxu.com/wordpress/2019/11/css-backdrop-filter/

[5]

backdrop-mask (codepen.io): https://codepen.io/xboxyan/pen/porpoXJ

[6]

CSS MASK COMPOSITE(遮罩合成)https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite

[7]

-webkit-mask-composite: https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-mask-composite

[8]

backdrop-mask-composite (codepen.io): https://codepen.io/xboxyan/pen/ExvMpQB

[9]

SVG在线压缩合并工具: https://www.zhangxinxu.com/sp/svgo/

[10]

backdrop-star (codepen.io): https://codepen.io/xboxyan/pen/vYJPaVy

[11]

backdrop-heart (codepen.io): https://codepen.io/xboxyan/pen/KKvEBjb


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存